查看原文
其他

宋宝华:让Linux的段错误(segmentation fault)不再是一个错误

宋宝华 Linux阅码场 2022-12-14

今天周末,娃儿们配合不闹事,写一篇短小精悍的文章吧,反正文章长了大家也没时间看。今天文章的目标是,如何在进程访问空指针等情况下,产生段错误后,不再退出而是继续运行。


这件事情,对于熔断(meltdown)漏洞的利用是比较关键的,我们都知道,利用meltdown漏洞,在用户空间是可以偷窥到内核空间数据的,下面的应用程序访问内核地址k:

meltdown漏洞攻击concept代码



array[256];

int c = *k; //k是内核地址

array[c]; //这句话产生了array[c] cache命中的副作用


之后,我们判断array[0..255]里面谁访问的最快,利用这种时间旁路攻击,获知k地址存放的是c。


在meltdown漏洞的分析里,我们重点关注了时间旁路攻击的好玩的地方,尤其以“李小璐汉堡问题”,说清楚了这种攻击的原理。但是我们忽略了一个事实,int c = *k; 这句话,本身,是一定会引起段错误的,因为用户空间做了越权访问。


其实,在熔断漏洞的代码中,对SIGSEGV信号,进行了特殊的处理。它截获了SIGSEGV的信号:

它实际捕获了SIGSEGV信号,在信号发生后,修改了CPU的跳转地址。


众所周知,在Linux里面,进程对信号的处理有三种方法,捕获、忽略、缺省行为。一般情况下,你什么都不做,行为就是缺省的,比如CTRL+C就会让进程死。


但是下面的进程,ctrl+c之后不仅死不了,还能打印东西:

原因很简单,它的2号信号SIGINT被我们捕获了。

这个从ps命令里面也可以看出(留意IGNORED和CAUGHT这2列):

当然,我们也可以完全不鸟它,选择忽略:

这个时候,按ctrl+c就什么鬼都没有:

我们还是可以ps一下(留意IGNORED和CAUGHT这2列):


在这么多信号里面,除了极少数信号类似SIGKILL、SIGSTOP这种不能捕获和忽略以外,其他的都是可以被我们拿来把玩的,当然也包括看起来牛逼轰轰的SEGV段错误(编号11)的信号。



我必须反复强调一点,当你用ctrl+c等对应的信号去杀死一个进程A的时候,从来都不是你杀死了A,而是你给A发了个信号,而A在响应这个信号的时候,其对应行为是进程exit。所以,不是你杀死了进程A,而是你发个信号,“通知”目标进程A去死。所以你不能用人类世界的杀死来理解Linux的杀死。Linux的逻辑类似: 你对进程A说:“你去死吧”(发个可以让它死的信号),A看到这个pending的信号后,啥废话都不说,立即闷声死翘翘!所以,你只要改变A的响应行为,就可以选择不死。


下面的这段代码,利用了setjmp和longjmp来实现段错误后继续执行:

setjmp这个函数的原理是:

调用这个函数的时候,它会保存执行现场,并返回0;之后调用longjmp,可恢复到setjmp保存的现场,setjmp再次返回,不过这次该函数返回非0。我们在代码里面标注下1-5的时间轴可能能方便大家理解:

第1和4步都在setjmp这个位置。上述例子中的关键点是,对SIGSEGV进行了捕获,并在SEGV的信号处理函数中,进行了代码的跳转。所以它的执行结果如下:

如果我们删除了绑定SIGSEGV的这行代码:

则执行结果为:

在执行*p = 0的时候,得到了我们期待的段错误。


(您的打赏是小弟持续原创的动力 ^-^)



Linux阅码场原创精华文章汇总

更多精彩,尽在"Linux阅码场",扫描下方二维码关注

点一点右下角”在看”,为阅码场打Call~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存